/* 组件挂载、更新 */
import { patch, Types } from "./render.js";
import { reactive, shallowReactive, shallowReadonly } from "./reactive.js";
import { effect } from "./effect.js";
import { ref, shallowRef } from "./ref.js";
import { createElement } from "../utils/browserDOMAPI.js";

/* 任务缓存队列，用一个Set数据结构来表示，这样就可以自动对任务进行去重 */
const queue = new Set();

/* 代表是否正在刷新任务队列 */
let isFlushing = false;

/* 立即 resolve 的 Promise 实例 */
const p = Promise.resolve();

/* 当前组件的实例 */
let currentInstance = null;

/* 设置当前的组件实例 */
function setCurrentInstance(instance) {
  currentInstance = instance;
}

/**
 * @description 调度器函数，用来将一个任务添加到任务缓存队列中，并开始刷新队列
 * @param {*} job 任务函数
 */
function queueJob(job) {
  /* 将任务函数添加到任务缓存队列中 */
  queue.add(job);
  /* 如果还没有开始刷新队列，则开始刷新 */
  if (!isFlushing) {
    /* 将标志改为true，表示正在刷新中，避免重复刷新 */
    isFlushing = true;
    /* 在微任务队列中刷新缓存队列 */
    p.then(() => {
      queue.forEach((job) => job());
    }).finally(() => {
      /* 重置状态 */
      isFlushing = false;
      queue.clear();
    });
  }
}

/**
 * @description 区分并解析出组件的attrs和props属性
 * @param {*} propsOptions 组件定义的props
 * @param {*} propsData 传给组件的props
 * @returns Array [props, attrs]
 */
function resolveProps(propsOptions, propsData) {
  const props = {};
  const attrs = {};
  const keys = Object.keys(propsData);

  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    if (key in propsOptions || key.startsWith("on")) {
      props[key] = propsData[key];
    } else {
      attrs[key] = propsData[key];
    }
  }

  return [props, attrs];
}

/**
 * @description 判断新旧props是否有变化
 * @param {*} oldProps 旧props
 * @param {*} newProps 新props
 * @returns Boolean
 */
function hasChangedProps(oldProps, newProps) {
  /* 旧的props属性的数量 */
  const oldPropsKeys = Object.keys(oldProps);
  /* 新的props属性的数量 */
  const newPropsKeys = Object.keys(newProps);
  /* 如果传递给子组件的新旧的props属性的数量不一致，那么表示有变化需要更新 */
  if (oldPropsKeys.length !== newPropsKeys.length) {
    return true;
  }

  /* 数量相等的情况下，对比新旧props的属性值是否相等 */
  for (let i = 0; i < newPropsKeys.length; i++) {
    /* 新的props属性的key */
    const key = newPropsKeys[i];
    const newValue = newProps[key];
    const oldValue = oldProps[key];
    if (newValue !== oldValue) {
      return true;
    }
  }

  return false;
}

/* onMounted生命周期钩子函数 */
export function onMounted(fn) {
  if (currentInstance) {
    currentInstance.mounted.push(fn);
  } else {
    console.error("onMounted函数只能在setup函数中使用");
  }
}

/**
 * @description onUnmounted 生命周期函数
 * @param {*} fn 回调函数
 */
export function onUnmounted(fn) {
  if (currentInstance) {
    currentInstance.unmounted.push(fn);
  } else {
    console.error("onUnmounted函数只能在setup函数中使用");
  }
}

/**
 * @description 挂载组件
 * @param {*} vnode 虚拟DOM节点
 * @param {*} container 挂载容器
 * @param {*} anchor 挂载的参考DOM节点
 */
export function mountComponent(vnode, container, anchor) {
  /* 检查是否是函数式组件 */
  const isFunctional = typeof vnode.type === "function";

  /* 通过 vnode.type 获取到组件对象 */
  let componentOptions = vnode.type;

  if (isFunctional) {
    /* 如果是函数式组件，则将 vnode.type 作为 render渲染函数，vnode.type.props作为props即可 */
    componentOptions = {
      render: vnode.type,
      props: vnode.type.props,
    };
  }
  /* 获取组件的渲染函数 */
  let {
    setup,
    render,
    data,
    props: propsOptions,
    beforeCreate,
    created,
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
  } = componentOptions;

  /* 组件生命周期函数：组件创建之前 */
  beforeCreate && beforeCreate();

  /* 将组件自身的状态包装成一个响应式数据 */
  const state = data ? reactive(data()) : null;

  /* 插槽slots */
  const slots = vnode.children || {};

  /* 组件的props属性 */

  let props, attrs;

  if (vnode.props) {
    [props, attrs] = resolveProps(propsOptions, vnode.props);
  }

  /* 组件实例 */
  const instance = {
    /* 组件自身的状态 */
    state,
    /* 组件的props属性 */
    props: props && shallowReactive(props),
    /* 组件挂载的状态：默认为false，表示未挂载 */
    isMounted: false,
    /* 组件的虚拟DOM，即：subtree */
    vnode: null,
    /* 插槽 */
    slots,
    /* 需要执行的onMounted生命周期钩子函数的列表 */
    mounted: [],
    /* 需要执行的onUnmounted生命周期钩子函数的列表 */
    unmounted: [],
    /* keepAliveCtx keepAlive组件的上下文，只有KeepAlive组件才有 */
    keepAliveCtx: null,
  };

  /* 检查当前要挂载的组件是否为KeepAlive组件 */
  const isKeepAlive = vnode.type.__isKeepAlive;

  if (isKeepAlive) {
    instance.keepAliveCtx = {
      move(vnode, container, anchor) {
        insert(vnode.component.vnode.el, container, anchor);
      },
      createElement,
    };
  }

  /* 将组件实例设置到vnode上，用于后续更新 */
  vnode.component = instance;

  /* 事件发射器：emit */
  function emit(event, ...args) {
    console.log(args);
    /* 事件名称 */
    const eventName = `on${event[0].toUpperCase()}${event.slice(1)}`;

    console.log("🚀 ~ emit ~ eventName:", eventName);
    /* 获取到自定义事件的处理函数 */
    const handle = instance.props[eventName];
    /* 执行自定义使劲按处理函数 */
    if (handle) {
      console.log(...args);
      handle(...args);
    } else {
      console.error(`事件${event}不存在`);
    }
  }

  /* 创建setup上下文 */
  const setupContext = { attrs, emit, slots };

  /* 在setup函数调用之前，设置当前组件实例 */
  setCurrentInstance(instance);

  /* 调用setup函数，获取setup函数的返回值 */
  /* 因为需要防止篡改props的属性值，所以需要使用shallowReadonly对props进行包装，包装成浅层只读的代理对象 */
  let setupResult =
    setup &&
    setup(instance.props && shallowReadonly(instance.props), setupContext);

  /* setup函数调用之后，重置当前组件实例 */
  setCurrentInstance(null);

  /* 用来存放setup函数返回的供渲染函数使用的对象 */
  let setupState = null;

  /* setup 函数的返回值有两种情况：1.函数，2.供渲染函数使用的数据对象 */
  if (setupResult) {
    if (typeof setupResult === "function") {
      if (render)
        console.error("setup函数返回了渲染函数，render选项将会被忽略");
      render = setupResult;
    } else {
      setupState = setupResult;
    }
  }

  /* 创建组件实例的代理对象，因为 组件的props和自身的状态state都需要在渲染函数中通过this使用 */
  const renderContext = new Proxy(instance, {
    get(target, key, receiver) {
      const { state, props, slots } = target;

      if (key === "$slots") return slots;
      if (state && key in state) {
        return state[key];
      } else if (props && key in props) {
        return props[key];
      } else if (setupState && key in setupState) {
        return setupState[key];
      } else {
        console.log("不存在");
      }
    },
    set(target, key, value, receiver) {
      const { state, props } = target;

      if (state && key in state) {
        state[key] = value;
      } else if (props && key in props) {
        props[key] = value;
      } else if (setupState && key in setupState) {
        setupState[key] = value;
      } else {
        console.log("不存在");
      }
    },
  });

  /* 组件生命周期函数：组件创建完毕 */
  created && created.call(renderContext);

  /* 组件状态的自更新，当组件状态发生变化的时候，重新执行effect函数从而触发组件更新 */
  effect(
    () => {
      /* 调用组件的 render 渲染函数，获取虚拟DOM节点，并且获取到包装过后的响应式数据，将render函数执行时的this指向该响应式数据 */
      const subtree = render.call(renderContext, renderContext);
      /* 组件如果时第一次挂载 */
      if (!instance.isMounted) {
        /* 组件生命周期函数：组件挂在之前 */
        beforeMount && beforeMount.call(renderContext);
        /* 将虚拟DOM渲染成页面中真实的DOM节点 */
        patch(null, subtree, container, anchor);
        /* 更新组件实例 isMounted 状态为 true，表示组件已挂载 */
        instance.isMounted = true;
        /* 组件生命周期函数：组件挂在完毕 */
        instance.mounted &&
          instance.mounted.forEach((hook) => hook.call(renderContext));
        // mounted && mounted.call(renderContext);
      } else {
        /* 组件更新 */
        /* 组件生命周期函数：组件更新之前 */
        beforeUpdate && beforeUpdate.call(renderContext);
        /* 组件更新 */
        patch(instance.vnode, subtree, container, anchor);
        /* 组件生命周期函数：组件更新完毕 */
        updated && updated.call(renderContext);
      }

      /* 更新组件的vnode虚拟DOM */
      instance.vnode = subtree;
    },
    {
      scheduler: queueJob,
    }
  );

  /**
   * 下面更新了四次 state.text 的值，但只会触发一次副作用函数。
   * 这是因为副作用函数被添加到异步任务队列中，而状态的更新是同步的。
   * 并且，副作用函数会被添加到Set数据结构的队列中进行缓存起来，达到去重的效果。
   * 用一个标识进行标记，正在刷新任务队列时，则不会再进行刷新任务队列
   * 所以对状态进行更新的话，最后副作用函数只会执行一次。
   * 但是会发现，重新创建了一个新的DOM节点，这是因为每次触发副作用函数时，都会执行patch函数，但是patch的第一个参数总是null，所以会创建新的DOM节点。
   * 为了解决这个问题，需要添加“组件实例”
   *  */
  // state.text = "组件状态更新1";
  // state.text = "组件状态更新2";
  // state.text = "组件状态更新3";
  // state.text = "组件状态更新4";

  // state.name = "我是李四";
}

/**
 * @description 更新组件
 * @param {*} oldVnode 旧的虚拟DOM节点
 * @param {*} vnode 新的虚拟DOM节点
 * @param {*} anchor 插入到文档对象的参考DOM节点
 */
export function patchComponent(oldVnode, vnode, anchor) {
  /* 获取组件实例，因为是相同的组件，新的组件没有挂载所以没有组件实例，所以使用旧的组件实例 */
  const instance = (vnode.component = oldVnode.component);
  /* 获取组件实例的props */
  const { props } = instance; // 相当于是 const props = instance.props
  /* 检测传递给props子组件是否发生变化，如果没有变化则不需要更新 */
  if (hasChangedProps(oldVnode.props, vnode.props)) {
    /* 由父组件传的新的props数据和子组件定义的新的props结构获取子组件新的props */
    const [newProps] = resolveProps(vnode.type.props, vnode.props);
    /* 更新组件实例的props */
    for (const key in newProps) {
      props[key] = newProps[key];
    }
    /* 删除不存在的props属性 */
    for (const key in props) {
      if (!(key in newProps)) {
        delete props[key];
      }
    }
  }
}

/**
 * @description 定义异步组件
 * @param {  Function | Object } options 配置对象
 * @param { Function } options.loader 加载器
 * @param { Function } options.timeout 超时时长
 * @param { Function } options.errorComponent 加载错误展示的组件
 * @returns Component
 */
export function defineAsyncComponent(options) {
  /* 如果options是一个加载器，将他包装到配置对象中 */
  if (typeof options === "function") {
    options = { loader: options };
  }
  const { loader } = options;
  /* 用来存储异步加载进来的组件 */
  let InnerComp = null;
  /* 返回一个组件 */
  return {
    name: "AsyncComponentWrapper",
    setup() {
      /* 标识异步组件是否已加载 */
      const loaded = ref(false);
      /* 是否超时 */
      const timeout = ref(false);
      /* 传给errorComponent组件的错误信息 */
      const errorMessage = shallowRef(null);
      /* 加载状态 */
      const loading = ref(false);
      /* loading定时器标识符 */
      const loadingTimer = null;
      /* 重试次数 */
      let retries = 0;
      /* 开启加载状态，如果用户有传延迟时间，则在延迟时间后再开启加载状态 */
      if (options.delay) {
        loadingTimer = setTimeout(() => {
          loading.value = true;
        }, options.delay);
      } else {
        /* 用户没有传延迟加载时间，则立马开启加载状态 */
        loading.value = true;
      }

      /* 封装加载异步组件函数（包含错误重试） */
      const load = () => {
        return loader().catch((error) => {
          /* 网络问题的错误信息 */
          errorMessage.value = error;
          /* 重试机制 */
          if (options.onError) {
            return new Promise((resolve, reject) => {
              /* 由用户来决定加载错误后的操作：重试/直接抛出错误 */
              const retry = () => {
                resolve(load());
                retries++;
              };
              const fail = () => reject(error);
              options.onError(retry, fail, retries);
            });
          } else {
            throw error;
          }
        });
      };

      load()
        .then((component) => {
          InnerComp = component.default;
          loaded.value = true;
        })
        .catch((error) => {
          console.log(error);
        })
        .finally(() => {
          loading.value = false;
          clearTimeout(loadingTimer);
        });

      /* 定时器标识符 */
      let timer = null;
      /* 如果用户设置了超时时长 */
      if (options.timeout) {
        timer = setTimeout(() => {
          timeout.value = true;
          /* 超时的错误信息 */
          const err = new Error(
            `Async component timed out after ${options.timeout}ms.`
          );
          errorMessage.value = err;
        }, options.timeout);
      }

      /* 组件卸载时清除定时器 */
      onUnmounted(() => clearTimeout(timer));

      /* 占位内容组件 */
      let placeholder = { type: Types.TEXT, children: "占位内容" };

      /* 返回异步组件加载内容 */
      return () => {
        /* 异步组件加载成功，渲染异步组件 */
        if (loaded.value) {
          return { type: InnerComp };
        } else if (errorMessage.value && options.errorComponent) {
          /* 如果超时了，则渲染用户指定的Error组件，如果没有指定的话就渲染placeholder组件 */
          return options.errorComponent
            ? {
                type: options.errorComponent,
                props: { error: errorMessage.value },
              }
            : placeholder;
        } else if (loading.value && options.loadingComponent) {
          /* 如果用户传了加载中展示的组件那么在加载异步组件的过程中展示加载状态组件 */
          return {
            type: options.loadingComponent,
          };
        }
        return placeholder;
      };
    },
  };
}

export const KeepAlive = {
  name: "KeepAlive",
  /* 用来标识这是keepalive组件 */
  __isKeepAlive: true,
  props: {
    include: {
      type: Array,
    },
    exclude: {
      type: Array,
    },
  },
  setup(props, { slots }) {
    /**
     * 创建一个缓存对象
     * key: vnode.type
     * value: vnode
     *  */
    const cache = new Map();

    /* 当前KeepAlive组件实例 */
    const instance = currentInstance;

    /**
     * 对于 KeepAlive 组件来说，它的实例对象上存在特殊的 keepAliveCtx 对象，该对象由渲染器注入，
     * 该对象会暴露渲染器的一些内部方法，其中 move 函数用来将一段 DOM 移动到另一个容器中。
     */
    const { move, createElement } = instance.keepAliveCtx;

    /* 创建隐藏容器 */
    const storageContainer = createElement("div");

    /**
     * keepAlive组件实例对象上会被添加两个内部函数，分别是_deActivate 和 _activate，
     * 这两个函数会在渲染器中被调用
     */

    /* 将被keepalive的组件移动到隐藏容器中 */
    instance._deActivate = (vnode) => {
      move(vnode, storageContainer);
    };

    /* 将被keepalive的组件从隐藏容器中启动到页面中去 */
    instance._activate = (vnode, container, anchor) => {
      move(vnode, container, anchor);
    };

    /* render渲染函数 */
    return () => {
      /* KeepAlive组件默认插槽中的内容就是要被keepalive的组件 */
      const rawVnode = slots.default();

      /* 判断默认插槽中的内容是否为组件，如果不是组件则直接渲染，因为非组件的虚拟节点无法被KeepAlive */
      if (
        typeof rawVnode.type !== "object" ||
        typeof rawVnode.type !== "function"
      ) {
        return rawVnode;
      }

      /* 获取内部组件的name属性 */
      const name = rawVnode.type.name;

      /* 如果传了include，并且include中没有name的值或者传了exclude，并且exclude中包含了name，那么不进行keepalive */
      if (
        (name && props.include && !props.include.includes(name)) ||
        (props.exclude && props.exclude.includes(name))
      ) {
        return rawVnode;
      }

      /* 在挂载时，先获取缓存中的组件 vnode */
      const cachedVnode = cache.get(rawVnode.type);

      /* 判断缓存中是否存在当前的组件 vnode */
      if (cachedVnode) {
        /* 如果缓存中存在，则不应该执行挂载，而应该执行激活 */
        /* 继承组件实例 */
        rawVnode.component = cachedVnode.component;
        /* 在 vnode 上添加keptAlive属性，标记为true， 表示该组件是被keepalive的组件，避免渲染器重新挂载它 */
        vnode.keptAlive = true;
      } else {
        cache.set(rawVnode.type, rawVnode);
      }

      /* 在组件 vnode 上添加 shouldKeepAlive 属性，并标记为true，避免渲染器真的将组件卸载 */
      rawVnode.shouldKeepAlive = true;
      /* 将 KeepAlive 组件的实例也添加到 vnode 上， 以便在渲染器中访问 */
      rawVnode.keepAliveInstance = instance;

      return rawVnode;
    };
  },
};
